package nz.ac.massey.jbuif.editor;

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Vector;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import nz.ac.massey.jbuif.editor.components.BaseComponent;
import nz.ac.massey.jbuif.editor.components.BlankInfoPanel;
import nz.ac.massey.jbuif.editor.components.ListEditorPanel;
import nz.ac.massey.jbuif.editor.components.ObjectEditorPanel;
import nz.ac.massey.jbuif.events.ObjectLoadedEvent;
import nz.ac.massey.jbuif.events.ObjectLoadedListener;
import nz.ac.massey.jbuif.utils.Utils;


/**
 * class bean editor
 * @author AMIR H KIOUMARS
 *
 */
public class BeanEditor extends JPanel implements ActionListener {

	private static final long serialVersionUID = -2713695925136244254L;
	
	private JPanel _beanForm;
	private HashMap<String, String> supportedClasses = null;
	private Object thisBean;

	private Vector<ObjectLoadedListener> objLoadedListeners;
	private Collection<String> availableBeans = null;
	private Collection<Object> objectInstances = null;
	
	private ClassLoader myClassLoader;
	private JFrame objectChooserFrame;
	private JComboBox objectChooserCB;
	
	private ObjectEditorPanel objectEditorRef;

	
	/**
	 * default constructor
	 */
	public BeanEditor() {
		super();

		objLoadedListeners = new Vector<ObjectLoadedListener>();

		initSupportedClasses();

		_beanForm = this; // why ?
		_beanForm.setLayout(new BoxLayout(_beanForm, BoxLayout.Y_AXIS));
	}
	
	
	/**
	 * setClassLoader
	 * @param cl ClassLoader instance
	 */
	public void setClassLoader(ClassLoader cl){
		this.myClassLoader = cl;
	}
	
	
	/**
	 * initSupportedClasses initialize the supported classs
	 */
	private void initSupportedClasses()
	{
		// could potentially load these dynamically from a config file, and load the appropriate editor .class file.
		
		supportedClasses = new HashMap<String, String>();

		supportedClasses.put("java.lang.String",
				"nz.ac.massey.jbuif.editor.components.TextEditorPanel");
		supportedClasses.put("java.util.Date",
				"nz.ac.massey.jbuif.editor.components.DateEditorPanel");
		supportedClasses.put("java.lang.Integer",
				"nz.ac.massey.jbuif.editor.components.IntEditorPanel");
		supportedClasses.put("int",
				"nz.ac.massey.jbuif.editor.components.IntEditorPanel");
		supportedClasses.put("java.lang.Double",
				"nz.ac.massey.jbuif.editor.components.DoubleEditorPanel");
		supportedClasses.put("double",
				"nz.ac.massey.jbuif.editor.components.DoubleEditorPanel");
		supportedClasses.put("java.util.Collection",
		"nz.ac.massey.jbuif.editor.components.ListEditorPanel");
		supportedClasses.put("java.util.Boolean",
		"nz.ac.massey.jbuif.editor.components.BooleanEditorPanel");
		supportedClasses.put("java.lang.Boolean",
		"nz.ac.massey.jbuif.editor.components.BooleanEditorPanel");
	}

	
	/**
	 * addObjectLoadedListener
	 * @param oll ObjectLoadedListener instance
	 */
	public void addObjectLoadedListener(ObjectLoadedListener oll) {
		objLoadedListeners.add(oll);
	}

	
	/**
	 * removeObjectLoadedListener
	 * @param oll ObjectLoadedListener instance
	 */
	public void removeObjectLoadedListener(ObjectLoadedListener oll) {
		objLoadedListeners.remove(oll);
	}

	
	/**
	 * fireObjectLoadedEvent
	 * @param ole ObjectLoadedEvent instance
	 */
	private void fireObjectLoadedEvent(ObjectLoadedEvent ole) {
		Object[] oLL = new Object[objLoadedListeners.size()];
		objLoadedListeners.copyInto(oLL);

		for (Object al : oLL) {
			((ObjectLoadedListener) al).onObjectLoaded(ole);
		}
	}

	
	/**
	 * getBean
	 * @return a bean
	 */
	public Object getBean() {
		return thisBean;
	}

	
	/**
	 * Generates and displays the editor for the given bean
	 * @param bean a bean object
	 * @param isHistoryItem if this is true, it means that the bean is from the history, and should not be added to the history again.
	 */
	@SuppressWarnings("unchecked")
	public void generateEditorForBean(Object bean, boolean isHistoryItem) {

		
		if (bean == null) {
			throw new IllegalArgumentException(
					"Parameter 'bean' cannot be null.");
		}
		
		_beanForm.removeAll();
		thisBean = bean;
		
		BeanInfo info = null;
		try {
			// Add the path to the bean info package
			// info when loading a jar dynamically.
			Introspector.setBeanInfoSearchPath(new String[] {
					"nz.example.uni.beaninfo", "sun.beans.infos" });
			info = Introspector.getBeanInfo(bean.getClass());
			
		} catch (IntrospectionException e) {
			e.printStackTrace(System.err);
			System.err.println("Problem introspecting bean");
		}

		for (PropertyDescriptor pd : info.getPropertyDescriptors()) {

			// System.out.print("Property: { N=" + pd.getName() + ", DN="
			//		+ pd.getDisplayName() + ", T="
			//		+ pd.getPropertyType().getName());

			String classType = supportedClasses.get(pd.getPropertyType()
					.getName());
			
			//This is the value of the Beans property if set.
			Object getResult = new Object();
			Method[] methods = bean.getClass().getMethods();

			//Find get methods, invoke them
			for (int i = 0; i < methods.length; i++) {		
				if (methods[i].getName().toLowerCase().contains(
						"get" + pd.getName().toLowerCase())) {
					try {
						getResult = methods[i].invoke(thisBean, new Object[] { });
					} catch (Exception e) {
						e.printStackTrace();
					} 
				}
			}
			//Done setting values.
			
			if (classType != null) {
				// Create Bean editor panel
				
				try {
					Class<?> newClass = Class.forName(classType);
					Constructor constructor = newClass
							.getConstructor(new Class[] { BeanEditor.class,
									String.class, String.class, Object.class});

					Object newPanel = constructor.newInstance(new Object[] {
							this, pd.getName(), pd.getDisplayName(),getResult });

					JPanel panel = ((BaseComponent) newPanel).getPanel();
					_beanForm.add(panel);
					
					//Set type of items in Collection????
					if(panel instanceof ListEditorPanel){
						//ListEditorPanel lep = (ListEditorPanel)panel;
						//lep.setCollectionType("nz.example.uni.Paper");
						//lep.setClassLoader(this.myClassLoader);
					}
					
				} catch (Exception e) {
					e.printStackTrace();
					System.err.println("Problem loading class into BeanEditor");
				}
			} else if (availableBeans.contains(pd.getPropertyType().getName())){
				
				// Bean is part of the loaded classes
				
				ObjectEditorPanel objPanel = new ObjectEditorPanel(this,pd.getName(),pd.getDisplayName(),getResult);
				objPanel.setPropertyType(pd.getPropertyType().getName());
				_beanForm.add(objPanel.getPanel());

			} else if (pd.getPropertyType().getName() != "java.lang.Class") {
				// If not plain 'class' show no editor available message
				BlankInfoPanel blankPanel = new BlankInfoPanel(this, pd
						.getName(), pd.getDisplayName(),null);
				blankPanel.setPropertyType(pd.getPropertyType().getName());
				blankPanel.repaint();
				_beanForm.add(blankPanel.getPanel());
			}
		}
		
		//Test list panel
		/*
		String[] mytempArray = new String[] {"Daniel","Andrew","Amir"};
		ArrayList myTempAL = new ArrayList();
		myTempAL.add("Daniel");
		myTempAL.add("Andrew");
		myTempAL.add("Amir");
		ListEditorPanel blankPanel = new ListEditorPanel(this, "String", "String List",myTempAL);
		blankPanel.setCollectionType("nz.example.uni.Paper");
		blankPanel.setClassLoader(this.myClassLoader);
		blankPanel.repaint();
		_beanForm.add(blankPanel.getPanel());
		*/
		
		_beanForm.validate();
		_beanForm.repaint();

		// fire event to indicate that bean has been loaded for editing
		fireObjectLoadedEvent(new ObjectLoadedEvent(this, thisBean, isHistoryItem));
	}

	
	/**
	 * clearEditor clear the editor
	 */
	public void clearEditor() {
		this.thisBean = null;
		
		_beanForm.removeAll();
		JLabel testLabel = new JLabel();
		testLabel.setText("  Nothing being edited.");
		_beanForm.add(new JPanel().add(testLabel));
		_beanForm.repaint();
		_beanForm.revalidate();
	}

	/**
	 * Validates that a property is valid before it is saved to the bean.
	 * @param string type
	 * @param string property
	 * @return true/false resulting the validation of a property
	 */
	public boolean validateProperty(String type, String property) {

		if (type.matches("int") && property.matches("[0-9]+")) {
			return true;
		} else if (type.matches("double")
				&& property.matches("([0-9]+)|(([0-9]+)\\.([0-9]+))")) {
			return true;
		}
		// when stuff chances check
		// property change events
		return false;
	}

	// should also fire property change events once a property change has been
	// validated.

	@Override
	public void actionPerformed(ActionEvent e) {
		
		if(e.getActionCommand().matches("ObjectSelected")||e.getActionCommand().matches("objectChooserOK")){
			//This is called when the popup Object chooser selects an item
			JComboBox cb = objectChooserCB;
			
			objectChooserFrame.setVisible(false);
			
			updateProperty(cb.getName(), cb.getSelectedItem());
			
			//Update the Panel itself. set its value and redisplay its text
			if(objectEditorRef!=null){
				objectEditorRef.setPropertyValue(cb.getSelectedItem());
				objectEditorRef.redrawText();
			}			
			return;
		}
		
		if(e.getActionCommand().matches("objectChooserCancel")){
			objectChooserFrame.setVisible(false);
		}
		
		// get changed object, use changed value after validation
		String type = e.getSource().getClass().getName();

		//Action performed on child panels (propertys)
		//Get the changed property, and update bean
		if(supportedClasses.containsValue(type)){
			BaseComponent tempObject = (BaseComponent)e.getSource();
			
			if(tempObject.getPropertyValue()!=null){
				// System.out.println("Property changed " + tempObject.getPropertyName()
				//	+ " " + tempObject.getPropertyValue().toString());
				//Update bean here
				updateProperty(tempObject.getPropertyName(), tempObject.getPropertyValue());
			} else {
				// System.out.println("Update property: Invalid value passed");
			}
		}
		
		if (type.contains(".ObjectEditorPanel")) {
			
			ObjectEditorPanel tempObject = (ObjectEditorPanel) e.getSource();
			
			//Object editingObject = tempObject.getPropertyValue();
			// System.out.println("Type of combo item "+tempObject.getPropertyType());
			
			switch (tempObject.getSelectedIndex()){
			case -1:
				break;
			case 0:
				//null
				updateProperty(tempObject.getPropertyName(), null);
				break;
			case 1:
				//Use existing
				//popup window select from exiting of that type
				objectChooserFrame(tempObject);
				break;
			case 2:
				//New
				Object beanInst = Utils.createBeanFromName(tempObject.getPropertyType(), myClassLoader);	
				tempObject.setPropertyValue(beanInst);
				updateProperty(tempObject.getPropertyName(), beanInst);
				tempObject.setTextField();
				objectInstances.add(beanInst);
				this.generateEditorForBean(beanInst,true);
				break;
			case 3:
				//Edit
				if(tempObject.getPropertyValue()!=null){
					this.generateEditorForBean(tempObject.getPropertyValue(),true);
				} else {
					// System.out.println("Bean to edit is null, creating a new one");
					Object beanInst2 = Utils.createBeanFromName(tempObject.getPropertyType(), myClassLoader);	
					tempObject.setPropertyValue(beanInst2);
					updateProperty(tempObject.getPropertyName(), beanInst2);
					tempObject.setTextField();
					objectInstances.add(beanInst2);
					this.generateEditorForBean(beanInst2,true);
				}
				break;
			default:
				break;
			}
			tempObject.redrawText();
			
			//System.out.println("Int Text field changed "+ tempText.getPropertyName() + " " + tempText.getNumber());
			//updateProperty(tempText.getPropertyName(), tempText.getNumber());
		}
		
		if (type.contains(".ListEditorPanel")) {
			
			ListEditorPanel tempObject = (ListEditorPanel) e.getSource();
			//Update the beans collection
			updateProperty(tempObject.getPropertyName(), tempObject.getPropertyValue());
		}
		
		//Something changed, update the listview display
		this.firePropertyChange("EditedBean", null, this.thisBean.toString());
	}
	
	// Take the following parameters and update the bean object
	@SuppressWarnings("unchecked")
	private void updateProperty(String methodName, Object propertyValue) {
		
		if (propertyValue == null || thisBean == null) {
			return;
		}

		try {
			// System.out.println("Supplied class name:"
			//		+ thisBean.getClass().getName());
			Class myclass = Class.forName(thisBean.getClass().getName(),true,this.myClassLoader);

			// Use reflection to list methods and invoke them
			Method[] methods = myclass.getMethods();

			for (int i = 0; i < methods.length; i++) {
				if (methods[i].getName().toLowerCase().equals(
						"set" + methodName.toLowerCase())) {
					methods[i].invoke(thisBean, new Object[] { propertyValue });
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			// System.out.println("Failed to update bean property");
		}
	}

	
	/**
	 * setBeansAvailable set the available beans
	 * @param availableBeans collection of string
	 */
	public void setBeansAvailable(Collection<String> availableBeans) {
		this.availableBeans = availableBeans;
	}
	
	
	/**
	 * getBeansAvailable get the available beans
	 * @return the collection of string of available beans
	 */
	public Collection<String> getBeansAvailable() {
		return this.availableBeans;
	}

	
	/**
	 * setBeanInstances set the bean objects
	 * @param objects a collection of objects
	 */
	public void setBeanInstances(Collection<Object> objects) {
		this.objectInstances = objects;
	}
	
	
	/**
	 * objectChooserFrame 
	 * @param tempObject ObjectEditorPanel instance
	 */
	private void objectChooserFrame(ObjectEditorPanel tempObject){
		objectChooserFrame = new JFrame();
		JLabel objectChooserLabel = new JLabel();
		JPanel objectChooserPanel = new JPanel();
		objectChooserPanel.setLayout(new BoxLayout(objectChooserPanel, BoxLayout.Y_AXIS));
		objectChooserCB = new JComboBox();
		
		ArrayList<Object> filteredObjects = new ArrayList<Object>();
		
		objectChooserCB.setName(tempObject.getPropertyName());
		objectChooserCB.setActionCommand("ObjectSelected");
		objectChooserCB.addActionListener(this);
		for(Object o : objectInstances){
			if(o.getClass().getName().matches(tempObject.getPropertyType())){
				//System.out.println("Adding item to list "+o.toString());
				filteredObjects.add(o);
				objectChooserCB.addItem(o);
			}
		}
		objectChooserLabel.setText("Choose an object");
		
		objectChooserPanel.add(objectChooserLabel);
		objectChooserPanel.add(objectChooserCB);
		
		objectChooserFrame.setTitle("Object Chooser");
		objectChooserFrame.add(objectChooserPanel);
		objectChooserFrame.setSize(400, 150);
		objectChooserFrame.setVisible(true);
		objectEditorRef = tempObject;
		
		//Ok and cancel buttons
		JPanel buttonPanel = new JPanel(new FlowLayout());
		
		JButton okButton = new JButton();
		okButton.setActionCommand("objectChooserOK");
		okButton.addActionListener(this);
		okButton.setText("Ok");
		
		JButton cancelButton = new JButton();
		cancelButton.setActionCommand("objectChooserCancel");
		cancelButton.addActionListener(this);
		cancelButton.setText("Cancel");
		
		buttonPanel.add(okButton);
		buttonPanel.add(cancelButton);
		objectChooserPanel.add(buttonPanel);
	}
}
